Political Determinants of International Trade: Visualization

Author

Nikita Egorov

How do politics affect international trade?

Original study: The Political Determinants of International Trade: The Major Powers, 1907–1990 (James D. Morrow, Randolph M. Siverson and Tressa E. Tabares)

Data can be accessed on Harvard Dataverse.

It is hard to deny that politics influences international trade. But which geopolitical factors matter the most? In their study, The Political Determinants of International Trade, James D. Morrow, Randolph M. Siverson, and Tressa E. Tabares explore this question.

Using 20th-century data on trade volumes among major political powers, the authors test three key arguments about the relationship between politics and trade:

  1. Political allies trade more: States with aligned interests are more likely to engage in trade.

  2. Democracies trade more: Democratic states are more inclined to trade with one another than with non-democracies.

  3. Political rivalry reduces trade: Rivalries, especially in a bipolar system, hinder trade flows between states.

Data

The dataset includes annual observations of trade flows between all directed pairs of major powers (dyads) from 1907 to 1990, excluding the years during the two world wars and the year immediately following each war. The major powers include the United States, the United Kingdom, France, Germany, Russia and Italy. Since the dataset observed both multiple units and a prolonged period of time, it presents the case of a time-series cross-sectional data (TSCS).

The dataset is prepared specifically for regression analysis and structured for time-series cross-sectional modeling. The dependent variable, trade flows (XIJL), and key independent variables, such as GDP (GNPIL and CGNPJL), population (POPIL and POPJL), and distance (DISTANCL), are presented in log-transformed form.

The dataset also includes political variables of interest:

  • MIDL: A binary indicator for the presence of militarized disputes within the dyad.

  • DEMDL: A binary indicator for whether both members of the dyad are democracies.

  • ALLIAL, MULTIALL, BIALL: Binary indicators representing the existence of alliances (ALLIAL), with distinctions between multipolar (MULTIALL) and bipolar (BIALL) geopolitical systems.

  • TAUL: A measure of similarity in the foreign policy interests of the dyad, scaled between 0 and 1.

Show Code
library('dplyr')
library('ggplot2')
library('magrittr')
library('ggthemes')
library("plotly")
library('knitr')
library('ggiraph')

trade <- read.csv(file = "Trade.csv", header = TRUE)
# knitr::kable(head(trade))
Show Code
## create the categorical variables based on years
periods = c("Pre-WW1", "20s", "30s", "Post-WW2+50s", "60s", "70s", "80s")

trade <- trade %>%
  mutate(
    DECADE = case_when(YEAR <= 1913 ~ "Pre-WW1",
                       YEAR >= 1920 & YEAR < 1930 ~ "20s",
                       YEAR >= 1930 & YEAR <= 1939 ~ "30s",
                       YEAR >= 1947 & YEAR < 1960 ~ "Post-WW2+50s",
                       YEAR >= 1960 & YEAR < 1970 ~ "60s",
                       YEAR >= 1970 & YEAR < 1980 ~ "70s",
                       YEAR >= 1980 & YEAR <= 1990  ~ "80s",
                       TRUE ~ NA_character_
    ),
    DECADE = factor(DECADE, levels = periods)
  ) %>%
  mutate(YEAR = as.factor(YEAR))

trade %>%
  count(DECADE) %>%
  kable(
    caption = "Observations by Decade",
    col.names = c("Decade", "Count"),
    align = "lc"
  )
Observations by Decade
Decade Count
Pre-WW1 294
20s 420
30s 420
Post-WW2+50s 546
60s 420
70s 420
80s 462
Show Code
trade_summary <- trade %>%
  group_by(DECADE) %>%
  summarise(total_exports = sum(XIJL, na.rm = TRUE))

export_volume <- trade_summary %>%
  ggplot(aes(x = DECADE, y = total_exports)) +
  geom_col(fill = 'lightblue', color = 'navy') +
  scale_x_discrete(guide = guide_axis(angle = 20)) +
  theme_stata() +
  labs(
    x = "Decade",
    y = "Total Exports (million $)",
    title = "Total Trade Volume by Decade"
  )

ggplotly(export_volume)

Do democracies trade more?

One of the key assumptions tested in the study is that states with similar political regimes—particularly democratic dyads—are more likely to engage in trade. This hypothesis suggests that shared democratic values foster trust, reduce transaction costs, and create a favorable environment for trade partnerships.

In the study authors code a dyad democratic if both trading states were ranked 6+ on Gurr’s Institutionalized Democracy scale.

Show Code
trade <- trade %>%
  mutate(Dem = case_when(DEMDL == 0 ~ "Non-democratic dyad",
                         DEMDL != 0 ~ "Democratic dyad"))

trade %>%
  count(Dem) %>%
  kable(
    caption = "Number of observations per dyad type",
    col.names = c("Decade", "Count"),
    align = "lc"
  )
Number of observations per dyad type
Decade Count
Democratic dyad 1554
Non-democratic dyad 1428
Show Code
dyad_summary <- trade %>%
  group_by(DECADE, Dem) %>%
  summarise(total_exports = sum(XIJL, na.rm = TRUE))

volume_dyads <- ggplot(data = dyad_summary,
         mapping = aes(x = DECADE,
                       y = total_exports, fill = Dem))  +
  geom_col(position = "dodge") +
  scale_x_discrete(guide = guide_axis(angle = 20), limits=periods) +
  theme_stata() +
  labs(x = "Decade",
       y = "Exports, constant mln $",
       title = "Trade volume: Democratic partners vs. others")

ggplotly(volume_dyads)
Show Code
dembar_faceted <- ggplot(data = dyad_summary,
       mapping = aes(x = total_exports, y = DECADE, color = Dem)) +
  geom_col() +
  theme_stata() +
  labs(x = "Exports, constant mln $",
       y = "Decades",
       title = "Exports of dyads") +
  facet_wrap( ~ Dem,
              nrow = 1) +
  scale_y_discrete(guide = guide_axis(angle = 30), limits=periods) +
  theme(legend.position='none')

ggplotly(dembar_faceted)

Do political rivals trade less?

It is hypothesized that states with poor political relations trade less, as trading with a rival state may be seen as counterproductive and potentially empowering an opponent. Since “rivalry” lacks a direct measure, the authors use a binary proxy variable to indicate whether the states in a dyad experienced militarized interstate disputes.

These disputes are recorded as occurring if at least one member of the dyad threatened to use military force against the other.

Show Code
trade <- trade %>%
  mutate(Disputes = case_when(MIDL == 0 ~ "No disputes",
                         MIDL != 0 ~ "Disputes occured"))

trade %>%
  count(Disputes) %>%
  kable(
    caption = "Number of observations per dyad type",
    col.names = c("Dispute occured?", "Count"),
    align = "lc"
  )
Number of observations per dyad type
Dispute occured? Count
Disputes occured 118
No disputes 2864
Show Code
tradeMID_year <- trade %>%
  group_by(Disputes, YEAR) %>%
  summarise(total_exports = sum(XIJL, na.rm = TRUE))


MID <- ggplot(data = tradeMID_year,
              mapping = aes(x = YEAR, y = total_exports, color = Disputes, group = Disputes)) +
  geom_line(size = 1.2) +
  theme_stata() +
  labs(
    x = "Year",
    y = "Volume of exports",
    title = "Exports of dyads: Do political conflicts affect trade?",
    color = "Dispute Status"
  ) +
  theme(axis.text.x = element_text(angle = 90, hjust = 0.5),
        legend.position = 'bottom')

ggplotly(MID)
Show Code
tradeMID_DECADE <- trade %>%
  group_by(Disputes, DECADE) %>%
  summarise(total_exports = sum(XIJL, na.rm = TRUE))

MID_DECADE <- ggplot(data = tradeMID_DECADE,
              mapping = aes(x = DECADE, y = total_exports, fill = Disputes, color = Disputes)) +
  geom_col() +
  theme_stata() +
  labs(x = "Decades",
       y = "Volume of exports",
       title = "Exports of dyads: Do political conflicts affect trade?") +
  facet_wrap( ~ Disputes, nrow = 1) +
  scale_x_discrete(guide = guide_axis(angle = 30), limits=periods) +
  theme(legend.position='none')

ggplotly(MID_DECADE)

Do states with similar interests trade more?

It is commonly argued that states with similar political interests are more likely to trade with each other. Shared interests are expected to incentivize states to engage in economic collaboration, leading to more intensive trade relations.

To measure the similarity of interests, the authors use the TAU indicator, which is based on the correlation of their alliance portfolios. TAU ranges from -1 (indicating complete dissimilarity in alliance portfolios) to 1 (indicating perfect similarity in alliance portfolios).

Show Code
TAU_hist <- ggplot(trade, aes(x = TAUL)) +
  geom_histogram(bins = 15, fill = "navy", color = "black", alpha = 0.7) +
  labs(x = "TAU Value", 
       y = "Frequency", 
       title = "Distribution of TAU (Common Interests) Values in the Dataset") +
  theme_stata()

ggplotly(TAU_hist)

A low TAU value means the two countries in the dyad have very different allies, so they likely have conflicting interests. For example, one might have military agreements with certain countries, while the other has no such agreements, showing they are not closely aligned. A high TAU value means the countries share many of the same allies, indicating they have similar interests and are more likely to cooperate with each other.


As we can see on a histogram, the largest portion of the sample has relatively low similarity of allies portfolio (~ 0.2 - 0.3)

Show Code
TAU_hist <- ggplot(data = trade, aes(x = TAUL, y = XIJL)) +
  geom_point(alpha = 0.5) +
  geom_smooth(method = "loess", color = "navy", se = FALSE) +
  # geom_smooth(method = "loess", color = "navy", se = TRUE, fill = "lightblue", level = 0.90)
  theme_minimal() +
  labs(x = "Similarity of Interests (TAU)", y = "State Exports, mln. constant U.S. dollars)",
       title = "Similarity of Interests Vs. Export Volume")

ggplotly(TAU_hist)

Do military allies trade more?

Countries that are military allies often have stronger economic ties. When two countries are part of the same military alliance, they are expected to trust each other and collaborate more, reducing political risks that might otherwise hinder trade.

The authors use a binary variable to indicate whether countries in a trading dyad have a formal alliance or not. Additionally, they introduce an interaction term to account for the global power structure, distinguishing between a Multipolar world (before World War II) and a Bipolar world (after World War II). Hypothetically, alliances in a multipolar world are not expected to significantly impact trade, whereas alliances in a bipolar world are thought to have a stronger positive effect.

Show Code
trade <- trade %>%
  mutate(
    Alliance = case_when(
      ALLIAL == 0 ~ "No alliance",
      ALLIAL != 0 ~ "Alliance"
    ),
    Period = case_when(
      as.numeric(as.character(YEAR)) < 1947 ~ "Pre-WW2",
      as.numeric(as.character(YEAR)) >= 1947 ~ "Post-WW2"
    )
  )

alliance_summary <- trade %>%
  group_by(Alliance, Period) %>%
  summarize(count = n(), .groups = "drop")
Show Code
alliance_summary$Period <- factor(alliance_summary$Period, levels = c("Pre-WW2", "Post-WW2"))

ALLIAL_barplot <- ggplot(alliance_summary, aes(x = Period, y = count, fill = Alliance)) +
  geom_bar(stat = "identity") +
  scale_fill_manual(values = c("lightblue", "steelblue")) +
  labs(title = "Alliance: Dyad observations before and after WW2",
       x = "Period", y = "Count of Dyads") +
  theme_stata() +
  theme(legend.title = element_text(size = 12), 
        legend.text = element_text(size = 10))

ggplotly(ALLIAL_barplot)
Show Code
alliance_volume <- trade %>%
  group_by(YEAR, Alliance) %>%
  summarize(avg_exports = mean(XIJL, na.rm = TRUE), .groups = "drop")

alliance_volume$YEAR <- as.numeric(as.character(alliance_volume$YEAR))

ALLIAL_volume <- ggplot(alliance_volume, aes(x = YEAR, y = avg_exports, color = Alliance, group = Alliance)) +
  geom_line() +
  geom_vline(xintercept = c(1939, 1947), linetype = "dashed", color = "red", size = 1)
  # geom_rect(aes(xmin = 1939, xmax = 1947, ymin = -Inf, ymax = Inf),
  #           fill = "red", alpha = 0.2) +
  labs(title = "Time Series of Exports by Alliance Status",
       x = "Year",
       y = "Exports (in millions of USD)",
       color = "Alliance Status") +
  scale_color_manual(values = c("lightblue", "steelblue")) +
  theme_stata() + 
  theme(axis.text.x = element_text(angle = 90, hjust = 0.5),
        legend.position = 'bottom')
NULL
Show Code
ggplotly(ALLIAL_volume)
Show Code
library(ggplot2)
library(ggiraph)

# Create the basic line plot with interactive tooltips
ALLIAL_volume <- ggplot(alliance_volume, aes(x = YEAR, y = avg_exports, color = Alliance, group = Alliance)) +
  geom_line_interactive(aes(tooltip = paste("Year: ", YEAR, "<br>Exports: ", round(avg_exports, 2))), size = 1) +  # Add interactive tooltips to lines
  labs(title = "Time Series of Exports by Alliance Status",
       x = "Year",
       y = "Exports (in millions of USD)",
       color = "Alliance Status") +
  scale_color_manual(values = c("lightblue", "steelblue")) +
  theme_minimal() + 
  theme(axis.text.x = element_text(angle = 90, hjust = 0.5),
        legend.position = 'bottom') +
  # Add vertical lines at 1937 and 1947
  geom_vline(xintercept = c(1937, 1947), linetype = "dashed", color = "red", size = 1)

# Convert the plot to an interactive ggiraph plot
interactive_plot <- girafe(ggobj = ALLIAL_volume)

# Show the plot
interactive_plot

Gravity Model of International Trade

For testing the hypotheses, the authors employ a gravity model of international trade, augmented with variables for the political determinants discussed earlier (democracy, rivalry, alliances, and similarity of interests). The gravity model is a well-established framework in economics that predicts the volume of trade flows between two countries based on three core components:

  1. Economic Size:

    • Measured as Gross National Product (GNP) in this study, economic size captures the production and consumption potential of the trading countries.

    • Larger economies are expected to trade more due to their greater capacity to produce goods and services (supply side) and their higher consumer demand (demand side).

  2. Physical Distance:

    • Distance acts as a proxy for trade costs, including transportation expenses, communication barriers, and time lags.

    • Greater distances typically reduce trade flows, as higher costs make trade less competitive.

  3. Population:

    • The authors include population to account for its dual role:

      • Supply Side: Larger populations may increase production capacity, especially in labor-intensive industries.

      • Demand Side: Larger domestic markets may reduce the need for trade, as countries with sizable populations can meet more of their needs internally.

    • In this study, population is expected to negatively correlate with trade, reflecting the potential self-sufficiency of larger nations.

The authors extend the model by introducing political variables to examine how non-economic factors influence trade flows. These additions enable the gravity model to test hypotheses about how democracy, political rivalry, alliances, and shared interests shape international economic relationships.

GNP and Trade

Show Code
# Fit a regression model
model <- lm(XIJL ~ GNPIL + CGNPJL, data = trade)

# grid of values for GNPIL and CGNPJL
grid <- expand.grid(
  GNPIL = seq(min(trade$GNPIL, na.rm = TRUE), max(trade$GNPIL, na.rm = TRUE), length.out = 50),
  CGNPJL = seq(min(trade$CGNPJL, na.rm = TRUE), max(trade$CGNPJL, na.rm = TRUE), length.out = 50)
)

# Predict trade volume for the grid
grid$XIJL <- predict(model, newdata = grid)

# Create a matrix for the surface
surface_matrix <- matrix(grid$XIJL, nrow = 50, ncol = 50)

# Add the surface to the 3D scatterplot
plot_ly() %>%
  add_markers(data = trade, x = ~GNPIL, y = ~CGNPJL, z = ~XIJL,
              type = 'scatter3d', mode = 'markers',
              marker = list(size = 3, color = ~XIJL, colorscale = 'Viridis')) %>%
  add_surface(x = ~unique(grid$GNPIL), y = ~unique(grid$CGNPJL), z = ~surface_matrix,
              colorscale = 'Blues', opacity = 0.5) %>%
  layout(scene = list(
    xaxis = list(title = "GNP of Exporter"),
    yaxis = list(title = "GNP of Importer"),
    zaxis = list(title = "Trade Volume")))
Show Code
library(patchwork)

scatter_exporter <- ggplot(trade, aes(x = GNPIL, y = XIJL)) +
  geom_point_interactive(aes(tooltip = paste("Exporter GNP:", GNPIL, "<br>Trade Volume:", XIJL)),
                         alpha = 0.6, color = "darkblue") +
  geom_smooth(method = "lm", color = "blue", se = FALSE) +
  labs(x = "GNP of Exporter", y = "Trade Volume (XIJL)",
       title = "Trade Volume vs Exporter GNP") +
  theme_minimal()

scatter_importer <- ggplot(trade, aes(x = CGNPJL, y = XIJL)) +
  geom_point_interactive(aes(tooltip = paste("Importer GNP:", CGNPJL, "<br>Trade Volume:", XIJL)),
                         alpha = 0.6, color = "darkred") +
  geom_smooth(method = "lm", color = "red", se = FALSE) +
  labs(x = "GNP of Importer", y = "Trade Volume (XIJL)",
       title = "Trade Volume vs Importer GNP") +
  theme_minimal()

girafe(ggobj = scatter_exporter + scatter_importer, width_svg = 10, height_svg = 5)

Distance and Trade

Show Code
# Create the scatterplot
scatter_distance <- ggplot(trade, aes(x = DISTANCL, y = XIJL)) +
  geom_point_interactive(aes(tooltip = paste("Distance:", DISTANCL, "<br>Trade Volume:", XIJL)),
                         alpha = 0.6, color = "blue") +
  geom_smooth(method = "lm", color = "darkblue", se = TRUE) +
  labs(x = "Log-Distance", y = "Trade Volume (XIJL)",
       title = "Distance vs Trade Volume") +
  theme_minimal()

# scatter_distance
girafe(ggobj = scatter_distance, width_svg = 10, height_svg = 5)

Population and Trade

Show Code
# Scatterplot: XIJL vs Exporter Population
scatter_exporter_pop <- ggplot(trade, aes(x = POPIL, y = XIJL)) +
  geom_point(alpha = 0.6, color = "darkblue") +
  geom_smooth(method = "lm", color = "blue", se = FALSE) +
  labs(x = "Exporter Population", y = "Trade Volume (XIJL)",
       title = "Trade Volume vs Exporter Population") +
  theme_minimal()

# Scatterplot: XIJL vs Importer Population
scatter_importer_pop <- ggplot(trade, aes(x = POPJL, y = XIJL)) +
  geom_point(alpha = 0.6, color = "darkred") +
  geom_smooth(method = "lm", color = "red", se = FALSE) +
  labs(x = "Importer Population", y = "Trade Volume (XIJL)",
       title = "Trade Volume vs Importer Population") +
  theme_minimal()

# Combine the two scatterplots side by side
girafe(ggobj = scatter_exporter_pop + scatter_importer_pop, width_svg = 10, height_svg = 5)

LM + Panel-Corrected Standard Errors

Show Code
colSums(is.na(trade))
    YEAR     DYAD     XIJL    GNPIL    POPIL    POPJL   ALLIAL    DEMDL 
       0        0      102      126       60       60        0        0 
DISTANCL     MIDL   CGNPJL     TAUL     STIN    BIALL MULTIALL   DECADE 
       0        0      126       12        0        0        0        0 
     Dem Disputes Alliance   Period 
       0        0        0        0 
Show Code
colMeans(is.na(trade)) * 100
     YEAR      DYAD      XIJL     GNPIL     POPIL     POPJL    ALLIAL     DEMDL 
0.0000000 0.0000000 3.4205231 4.2253521 2.0120724 2.0120724 0.0000000 0.0000000 
 DISTANCL      MIDL    CGNPJL      TAUL      STIN     BIALL  MULTIALL    DECADE 
0.0000000 0.0000000 4.2253521 0.4024145 0.0000000 0.0000000 0.0000000 0.0000000 
      Dem  Disputes  Alliance    Period 
0.0000000 0.0000000 0.0000000 0.0000000 

datatable(

missing_summary,

caption = “Missing Values Summary”,

options = list(pageLength = 10, autoWidth = TRUE)

)

Show Code
missing_summary <- data.frame(
  Variable = names(trade),
  Missing_Count = colSums(is.na(trade)),
  Missing_Percentage = colMeans(is.na(trade)) * 100
)

missing_summary %>%
  arrange(desc(Missing_Count)) %>%
  knitr::kable(
    format = "html",
    caption = "Missing Values Summary",
    col.names = c("Variable", "Missing Count", "Missing Percentage (%)"),
    digits = 2
  )
Missing Values Summary
Variable Missing Count Missing Percentage (%)
GNPIL GNPIL 126 4.23
CGNPJL CGNPJL 126 4.23
XIJL XIJL 102 3.42
POPIL POPIL 60 2.01
POPJL POPJL 60 2.01
TAUL TAUL 12 0.40
YEAR YEAR 0 0.00
DYAD DYAD 0 0.00
ALLIAL ALLIAL 0 0.00
DEMDL DEMDL 0 0.00
DISTANCL DISTANCL 0 0.00
MIDL MIDL 0 0.00
STIN STIN 0 0.00
BIALL BIALL 0 0.00
MULTIALL MULTIALL 0 0.00
DECADE DECADE 0 0.00
Dem Dem 0 0.00
Disputes Disputes 0 0.00
Alliance Alliance 0 0.00
Period Period 0 0.00
Show Code
trade_clean <- na.omit(trade)
Show Code
library(stargazer)

gmt_full <- lm(XIJL ~ MIDL + TAUL + DEMDL + MULTIALL + BIALL + GNPIL + POPIL + CGNPJL + POPJL + DISTANCL, data = trade_clean)

stargazer(gmt_full, type = "text")

===============================================
                        Dependent variable:    
                    ---------------------------
                               XIJL            
-----------------------------------------------
MIDL                         -1.359***         
                              (0.172)          
                                               
TAUL                         2.282***          
                              (0.234)          
                                               
DEMDL                        0.879***          
                              (0.095)          
                                               
MULTIALL                     -0.983***         
                              (0.165)          
                                               
BIALL                        -0.699***         
                              (0.153)          
                                               
GNPIL                        0.698***          
                              (0.032)          
                                               
POPIL                        -0.332***         
                              (0.070)          
                                               
CGNPJL                       0.514***          
                              (0.032)          
                                               
POPJL                        -0.428***         
                              (0.070)          
                                               
DISTANCL                     -0.221***         
                              (0.028)          
                                               
Constant                     -4.306***         
                              (0.354)          
                                               
-----------------------------------------------
Observations                   2,631           
R2                             0.618           
Adjusted R2                    0.617           
Residual Std. Error      1.104 (df = 2620)     
F Statistic         424.504*** (df = 10; 2620) 
===============================================
Note:               *p<0.1; **p<0.05; ***p<0.01